Skip to content

Fixes #728: Copy the tables of models made branchable by a resolver into new branches - #729

Open
bctiemann wants to merge 2 commits into
mainfrom
728-replicate-resolver-model-tables
Open

bctiemann wants to merge 2 commits into
mainfrom
728-replicate-resolver-model-tables

Conversation

@bctiemann

@bctiemann bctiemann commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes: #728

The router sends the queries of a model that a plugin's resolver makes branchable to the branch, but provisioning copied only the tables of object types with the branching feature, plus their M2M through tables. A resolver-only model has no such object type, so its table wasn't copied, and its queries fell through to main's table.

Changes:

  • get_tables_to_replicate() also includes the table of each registered model that isn't change-logged and that supports_branching() says is branchable, plus its M2M through tables, when any resolver is registered. Proxy, unmanaged and missing tables are skipped, and exempt_models still applies through supports_branching().
  • docs/plugin-development.md: a resolver returning True also gives new branches their own copy of the model's table and its M2M through tables.

Tests:

  • With a resolver registered for auth.Group, its table and auth_group_permissions are in get_tables_to_replicate(), and aren't when the model is exempt.
  • A branch provisioned with that resolver has its own auth_group, and renaming a group or adding a permission to it in the branch leaves main's unchanged.
  • Both fail on main.

Branches provisioned before this change still have no copy of these tables. That's the same situation as #683.

@bctiemann

Copy link
Copy Markdown
Contributor Author

@claude /review and report only on actionable issues.

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @bctiemann's task in 2m 17s —— View job


Review

  • Read changed files
  • Analyze the implementation
  • Post review findings

The fix is correct and the approach is sound. One actionable issue and one minor note.

Actionable: M2M through tables of resolver-opted-in models are not replicated

utilities.py:344–354 adds only the model's own table. The existing path for change-logged models (lines 327–333) explicitly walks local_many_to_many and adds each through-table too. The new path skips that step.

For example, auth.Group has a permissions M2M to auth.Permission. Its through table (auth_group_permissions) won't be replicated, so any M2M write inside the branch (adding/removing permissions from a group) will land on main's auth_group_permissions, not the branch's copy. The test validates row-level isolation for the base table but doesn't exercise M2M fields, so this bug would go undetected.

Fix: after adding meta.db_table, walk model._meta.local_many_to_many the same way the change-logged loop does:

if (
    meta.db_table not in tables
    and meta.db_table in existing_tables
    and meta.managed
    and not meta.proxy
    and not issubclass(model, ChangeLoggingMixin)
    and supports_branching(model)
):
    tables.add(meta.db_table)
    for m2m_field in meta.local_many_to_many:
        if hasattr(m2m_field, 'through'):
            tables.add(m2m_field.through._meta.db_table)
        else:
            tables.add(m2m_field._get_m2m_db_table(meta))

Fix this →

Minor: addCleanup in test_changes_in_branch_stay_in_branch is redundant

ResolverModelBranchTestCase extends FastTeardownTransactionTestCase, whose _fixture_teardown already calls _drop_branch_schemas and drops every schema matching the branch prefix. The explicit self.addCleanup(self._drop_schema, branch.schema_name) on line 239 is harmless (DROP SCHEMA IF EXISTS no-ops on the second call) but adds noise.

…anches

M2M writes follow the instance's database, so without the branch's own copy
they fell through to main's through table. Also drop a redundant schema
cleanup from the branch test; the test case's teardown already drops it.
@bctiemann

Copy link
Copy Markdown
Contributor Author

Related, for the same release: #732 adds the missing-table check this PR has on its new loop to the existing object-type path of get_tables_to_replicate() (found via netboxlabs/netbox-custom-objects#1045). Kept as a separate issue so this PR stays scoped to #728.

@arthanson arthanson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the one nit about imports, otherwise - nice work!

# A model that a plugin's resolver makes branchable without change logging has no branching
# feature, but the router sends its queries to the branch, so the branch needs its table too.
if _branching_resolvers:
from django.apps import apps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are being re-imported for no reason - I think the ChangeLoggingMixin is the only one that needs to be here

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tables of models made branchable by a plugin resolver aren't copied into new branches, so branch changes write to main

2 participants